using System;
using System.Collections.Generic;
using System.Reflection;
using System.Security;
using System.Security.Policy;

namespace Fadd.Plugins
{
    /// <summary>
    /// This class is responsible of loading all modules in a system.
    /// </summary>
    public class PluginManager<T> : IApplication where T : class, IPlugin
    {
        private const string DefaultPath = "*.dll";
        private readonly IApplication _pluginManager;
        private readonly List<T> _plugins = new List<T>();
        private string _pluginPath = DefaultPath;
        private readonly List<string> _ignoredPlugins = new List<string>();

        /// <summary>
        /// Triggered when all plugins have been loaded, but not started yet.
        /// </summary>
        public event EventHandler PluginsLoaded = delegate { };

        private static readonly Evidence DefaultEvidence = new Evidence(
            new object[] {new Zone(SecurityZone.Internet)},
            new object[] {});

        /// <summary>
        /// Initializes a new instance of the <see cref="PluginManager&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="application">Interface exposed to plugins.</param>
        public PluginManager(IApplication application)
        {
            _pluginManager = application;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PluginManager&lt;T&gt;"/> class.
        /// </summary>
        public PluginManager() : this(null)
        {
        }

        /// <summary>
        /// Access a loaded plugin.
        /// </summary>
        /// <value>plugin if found; otherwise null.</value>
        public T this[string pluginName]
        {
            get
            {
                foreach (T plugin in Plugins)
                {
                    if (string.Compare(plugin.PluginName, pluginName, true) == 0)
                        return plugin;
                }

                return null;
            }
        }

        /// <summary>
        /// Folder in which plugins resides.
        /// </summary>
        /// <remarks>Can also contain wildcards</remarks>
        /// <example>
        /// <code>
        /// pluginMgr.Path = "plugins\\plugin*.dll";
        /// </code>
        /// </example>
        public string PluginPath
        {
            get { return _pluginPath; }
            set { _pluginPath = value; }
        }

        /// <summary>
        /// All plugins that should not be loaded.
        /// </summary>
        public List<string> IgnoredPlugins
        {
            get { return _ignoredPlugins; }
        }

        /// <summary>
        /// A list with all loaded plugins.
        /// </summary>
        public IEnumerable<T> Plugins
        {
            get { return _plugins; }
        }

        /// <summary>
        /// Called when a plugin assembly is about to be loaded.
        /// </summary>
        /// <param name="typeInfo">Information about the assembly.</param>
        /// <returns>Evidence that the <see cref="Assembly"/> should be loaded with.</returns>
        /// <remarks>
        /// The assembly is used to determine which security settings a plugin <see cref="Assembly"/> should be loaded with.
        /// </remarks>
        protected virtual Evidence GetEvidence(PluginTypeInfo typeInfo)
        {
            EvidenceRequestedEventArgs eventArgs = new EvidenceRequestedEventArgs(typeInfo);

            //only our own empty delegate
            if (EvidenceRequested.GetInvocationList().Length == 1)
                return DefaultEvidence;

            return eventArgs.Evidence;
        }

        /// <summary>
        /// Loads modules with the correct <see cref="Evidence"/> and correct protection level.
        /// </summary>
        protected void LoadModules()
        {
            PluginFinder finder = new PluginFinder();
            try
            {
                IList<Type> type = new List<Type>();
                type.Add(typeof (T));
                finder.Find(_pluginPath, type);
            }
            catch (ReflectionTypeLoadException err)
            {
                Console.WriteLine(err);
                foreach (Exception e in err.LoaderExceptions)
                    Console.WriteLine(e);
            }
            catch (TypeLoadException err)
            {
                Console.WriteLine(err);
            }

            foreach (PluginTypeInfo typeInfo in finder.Plugins)
            {
                Evidence evidence = GetEvidence(typeInfo);
                if (evidence == null)
                    continue;

                Assembly assembly = Assembly.LoadFrom(typeInfo.Location, evidence);
                foreach (Type type in typeInfo.Types)
                {
                    if (!typeof (T).IsAssignableFrom(type))
                        continue;

                    T loadedPlugin = (T) Activator.CreateInstance(type);
                    OnPluginLoaded(loadedPlugin, assembly, typeInfo);
                    _plugins.Add(loadedPlugin);
                }
            }
        }

        /// <summary>
        /// Loads all modules that can be loaded (their dependencies have been loaded.)
        /// </summary>
        /// <param name="loaded"></param>
        /// <param name="dependent"></param>
        /// <returns></returns>
        /// <remarks>Call this method as long as it returns true.</remarks>
        private bool StartModules(IDictionary<string, T> loaded, IDictionary<string, T> dependent)
        {
            foreach (KeyValuePair<string, T> pair in dependent)
            {
                bool dependenciesMissing = false;
                foreach (string dependency in pair.Value.Dependencies)
                {
                    if (loaded.ContainsKey(dependency))
                        continue;
                    dependenciesMissing = true;
                    break;
                }
                if (dependenciesMissing)
                    continue;

                dependent.Remove(pair.Value.PluginName);
                loaded.Add(pair.Value.PluginName, pair.Value);
                StartPlugin(pair.Value);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Called when a plugin have been loaded.
        /// </summary>
        /// <param name="plugin">Module being loaded.</param>
        /// <param name="assembly">Assembly that the plugin exist in.</param>
        /// <param name="typeInfo">information about the plugin</param>
        /// <remarks>Plugin have not been started yet.</remarks>
        protected virtual void OnPluginLoaded(T plugin, Assembly assembly, PluginTypeInfo typeInfo)
        {
            PluginLoaded(this, new PluginLoadedHandlerEventArgs<T>(plugin, assembly, typeInfo));
        }

        /// <summary>
        /// Manually add a plugin
        /// </summary>
        /// <param name="plugin">Plugin to add</param>
        /// <remarks>
        /// Used if you want to inject a known plugin to let it be loaded when all dependencies have been met.
        /// </remarks>
        public void Add(T plugin)
        {
            Check.Require(plugin, "plugin");
            _plugins.Add(plugin);
        }

        /// <summary>
        /// Loads all modules
        /// </summary>
        public void Start()
        {
            LoadModules();
            PluginsLoaded(this, EventArgs.Empty);

            Dictionary<string, T> loadedModules = new Dictionary<string, T>(),
                                  dependencyModules = new Dictionary<string, T>();

            // Put all modules in the dependent side
            foreach (T plugin in Plugins)
                dependencyModules.Add(plugin.PluginName, plugin);

            // Load a plugin at a time (since foreach breaks when something is removed)
#pragma warning disable 642
            while (StartModules(loadedModules, dependencyModules)) ;
#pragma warning restore 642

            // Build error message with all plugins that could not be loaded due to missing dependencies.
            string dependenciesMissing = string.Empty;
            foreach (KeyValuePair<string, T> pair in dependencyModules)
            {
                string modules = string.Empty;
                foreach (string dependency in pair.Value.Dependencies)
                    modules += dependency + ", ";
                modules = modules.Remove(modules.Length - 2);

                dependenciesMissing += "Module '" + pair.Key + "' is missing one of the following dependencies: " +
                                       modules;
            }
            if (dependenciesMissing != string.Empty)
                throw new InvalidOperationException(dependenciesMissing);
        }

        /// <summary>
        /// Calls start in a <see cref="IPlugin"/>.
        /// </summary>
        /// <param name="plugin">plugin to start</param>
        /// <exception cref="ArgumentNullException"></exception>
        protected virtual void StartPlugin(T plugin)
        {
            Check.Require(plugin, "plugin");

            ApplicationRequestedEventArgs<T> args = new ApplicationRequestedEventArgs<T>(plugin);
            ApplicationRequested(this, args);
            plugin.Start(args.Application ?? _pluginManager ?? this);
            PluginStarted(this, new PluginStartedEventArgs<T>(plugin));
        }

        /// <summary>
        /// A plugin have been loaded into the system
        /// </summary>
        public event PluginLoadedHandler<T> PluginLoaded = delegate { };

        /// <summary>
        /// A plugin have been started.
        /// </summary>
        public event PluginStartedHandler<T> PluginStarted = delegate { };

        /// <summary>
        /// An plugin assembly is about to be loaded and the system needs to know which <see cref="Evidence"/> the plugin assembly should be loaded with.
        /// </summary>
        /// <remarks>
        /// Plugins are loaded into the Internet zone per default.
        /// </remarks>
        public event EvidenceRequestedHandler EvidenceRequested = delegate{};

        /// <summary>
        /// Called when a plugin is about to be started and we need to get the application interface
        /// that should be used when starting the plugin.
        /// </summary>
        public event ApplicationRequestedHandler<T> ApplicationRequested = delegate{};

    }
}
